package com.huanletao.dom4j;

import com.huanletao.annotation.AnnotationUtils;
import com.huanletao.dom4j.annotation.ElementFormat;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.text.ParseException;
import java.util.Date;
import java.util.Iterator;
import java.util.Objects;

/**
 * <p>
 * XmlDocumentResolver 文档解析器
 * 1.  将xml文档解析并填充到javabean中
 * 2.  解析类型重叠实现
 * 3.  解析支持泛型操作
 * <p>
 * author: admin
 * createTime: 2022-07-30 11:48
 * since: 1.0.0
 */
public abstract class DocumentResolver {

    private static final SAXReader saxReader = new SAXReader();

    private static final String namespace_prefix_separator = ":";

    /**
     * 解析xmlFile
     *
     * @param file
     * @param clazz
     * @param <T>
     * @return
     */

    public static <T> T resolve(File file, Class<T> clazz) throws DocumentException {
        Document document = null;
        try {
            document = saxReader.read(file);
        } catch (DocumentException e) {
            ExceptionUtils.rethrow(e);
        }
        assert document != null;
        return resolve(document.getRootElement(), clazz);
    }

    /**
     * 解析xmlFile
     *
     * @param inputStream
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T resolve(InputStream inputStream, Class<T> clazz) throws DocumentException {
        Document document = null;
        try {
            document = saxReader.read(inputStream);
        } catch (DocumentException e) {
            ExceptionUtils.rethrow(e);
        }
        return resolve(document.getRootElement(), clazz);
    }

    /**
     * 解析xml 字符串 填充javabean;
     *
     * @param xmlStr
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T resolve(String xmlStr, Class<T> clazz) throws DocumentException {
        Document rootDocument = null;
        try {
            rootDocument = DocumentHelper.parseText(xmlStr);
        } catch (DocumentException e) {
            ExceptionUtils.rethrow(e);
        }
        return resolve(rootDocument.getRootElement(), clazz);
    }

    /**
     * @param rootElement
     * @param clazz
     */
    @SuppressWarnings("all")
    public static <T> T resolve(Element rootElement, Class<T> clazz) throws DocumentException {

        XmlRootElement xmlRootElement = AnnotationUtils.findAnnotation(clazz, XmlRootElement.class);
        if (Objects.isNull(xmlRootElement)) {
            xmlRootElement = AnnotationUtils.findAnnotation(clazz.getSuperclass(), XmlRootElement.class);
        }

        String elementName = rootElement.getName();
        String prefix = rootElement.getNamespacePrefix();
        String name = xmlRootElement.name();
        if (StringUtils.contains(name, namespace_prefix_separator)) {
            name = StringUtils.substringAfter(name, namespace_prefix_separator);
        }
        if (name.equals(elementName)) {
            return fillPropertiesBean(rootElement, clazz);
        }

        Iterator<Element> elementIterator = rootElement.elementIterator();
        T resolveFillBean = null;
        while (elementIterator.hasNext()) {
            Element next = elementIterator.next();
            resolveFillBean = resolve(next, clazz);
        }

        return resolveFillBean;
    }


    /**
     * 填充值到javabean 中,
     *
     * @param rootElement
     * @param clazz
     * @param <T>
     * @return
     */

    private static <T> T fillPropertiesBean(Element rootElement, Class<T> clazz) throws DocumentException {
        T instance = null;
        try {
            instance = clazz.newInstance();
        } catch (Exception e) {
            ExceptionUtils.rethrow(e);
        }
        Field[] allFields = ArrayUtils.addAll(clazz.getSuperclass().getDeclaredFields(),
                clazz.getDeclaredFields());
        for (Field field : allFields) {
            Object finalValue = null;
            if (!field.isAnnotationPresent(XmlElement.class)) {
                continue;
            }
            Class<?> fieldType = field.getType();
            XmlElement xmlElement = field.getAnnotation(XmlElement.class);
            String prefix = rootElement.getNamespacePrefix();
            Element element = rootElement.element(StringUtils.isBlank(prefix) ? xmlElement.name() : xmlElement.name().replace(prefix + ":", ""));
            if (Objects.isNull(element)) {
                continue;
            }

            if (isFieldGenericType(clazz, instance, field, element)) continue;

            finalValue = getFinalValue(field, element);

            if (fieldType.isAnnotationPresent(XmlRootElement.class)) {
                Element childRootElement = rootElement.element(xmlElement.name());
                finalValue = fillPropertiesBean(childRootElement, fieldType);
            }

            try {
                PropertyDescriptor pd = new PropertyDescriptor(field.getName(), clazz);
                Method writeMethod = pd.getWriteMethod();
                writeMethod.invoke(instance, finalValue);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return instance;
    }

    private static <T> boolean isFieldGenericType(Class<T> clazz, T instance, Field field, Element element) {
        Object finalValue;
        Type fieldGenericType = field.getGenericType();
        if (!(fieldGenericType instanceof Class<?>)) {
            Type genType = clazz.getGenericSuperclass();
            Type params = ((ParameterizedType) genType).getActualTypeArguments()[0];
            try {
                Class<?> paramsType = ClassUtils.getClass(params.getTypeName());
                if (paramsType.isAnnotationPresent(XmlRootElement.class)) {
                    finalValue = fillPropertiesBean(element, paramsType);
                    PropertyDescriptor pd = new PropertyDescriptor(field.getName(), clazz);
                    Method writeMethod = pd.getWriteMethod();
                    writeMethod.invoke(instance, finalValue);
                }
            } catch (Exception e) {
                ExceptionUtils.rethrow(e);
            }
            return true;
        }
        return false;
    }

    private static Object getFinalValue(Field field, Element element) throws DocumentException {
        String tempValue = element.getText();
        Object finalValue = null;
        if (field.getType() == Boolean.class || field.getType() == Boolean.TYPE) {
            finalValue = Boolean.parseBoolean(tempValue);
        } else if (field.getType() == Long.class || field.getType() == Long.TYPE) {
            finalValue = Long.parseLong(tempValue);
        } else if (field.getType() == Integer.class || field.getType() == Integer.TYPE) {
            finalValue = Integer.parseInt(tempValue);
        } else if (field.getType() == BigDecimal.class) {
            finalValue = new BigDecimal(tempValue);
        } else if (field.getType() == Date.class) {
            finalValue = parseDate(field, tempValue);
        } else if (field.getType() == String.class) {
            finalValue = tempValue;
        } else if (field.getType() == Float.class || field.getType() == Float.TYPE) {
            finalValue = Float.parseFloat(tempValue);
        }
        return finalValue;
    }

    /**
     * 解析日期类型
     *
     * @param field
     * @param tempValue
     * @return
     */
    private static Date parseDate(Field field, String tempValue) throws DocumentException {
        ElementFormat properties = field.getAnnotation(ElementFormat.class);
        if (properties == null) {
            throw new DocumentException(String.format("Date.class field name is  %s requirement annotation ElementFormat to format date ", field.getName()));
        }
        Date value = null;
        try {
            value = DateUtils.parseDate(tempValue, properties.pattern());
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return value;
    }
}
